package com.progenies.ecg.asm31.util;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;

import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import com.progenies.ecg.asm31.model.codeparts.CodeBlockASM31;
import com.progenies.ecg.asm31.model.codeparts.IConfigurableConstructorObject;
import com.progenies.ecg.asm31.model.codeparts.IConfigurableMethodObject;
import com.progenies.ecg.generator.ClassGeneratorFactory;
import com.progenies.ecg.model.AbstractCodeObject;
import com.progenies.ecg.model.ClassObject;
import com.progenies.ecg.model.ConstructorObject;
import com.progenies.ecg.model.FieldObject;
import com.progenies.ecg.model.ParameterObject;
import com.progenies.ecg.model.codeparts.CodeBlock;
import com.progenies.ecg.model.codeparts.ICodePart;
import com.progenies.ecg.model.codeparts.LoopCodePart;
import com.progenies.ecg.model.codeparts.SwitchCodePart;

public final class GenerationUtils
{
	private GenerationUtils() {}
	
	
	
	public static int getAccessTranslated(int modifiers[])
	{
		int access=Opcodes.ACC_PUBLIC;
		if(modifiers!=null && modifiers.length>0)
		{
			access=modifiers[0];
			for(int i=1;i<modifiers.length;i++)
				access|=modifiers[i];
		}
		return access;
	}



	public static String translateClassName(String name)
	{
		return name.replace('.', '/');
	}



	public static String translateDescription(ParameterObject<?>[] parameters,	Class<?> returnTypeClass)
	{
		StringBuffer b=new StringBuffer();
		b.append("(");
		
		if(parameters!=null && parameters.length>0)
		{
			for(ParameterObject<?> param : parameters)
			{
				Type type=Type.getType(param.getClassType());
				//Special cases: wrapper classes
				if(param.getClassType()==Boolean.TYPE) type=Type.BOOLEAN_TYPE;
				else if(param.getClassType()==Byte.TYPE) type=Type.BYTE_TYPE;
				else if(param.getClassType()==Character.TYPE) type=Type.CHAR_TYPE;
				else if(param.getClassType()==Short.TYPE) type=Type.SHORT_TYPE;
				else if(param.getClassType()==Integer.TYPE) type=Type.INT_TYPE;
				else if(param.getClassType()==Long.TYPE) type=Type.LONG_TYPE;
				else if(param.getClassType()==Float.TYPE) type=Type.FLOAT_TYPE;
				else if(param.getClassType()==Double.TYPE) type=Type.DOUBLE_TYPE;
				
				b.append(type.getDescriptor());
			}
		}
		b.append(")");
		b.append(((Type)(returnTypeClass==null?Type.VOID_TYPE:Type.getType(returnTypeClass))).getDescriptor());
		
		return b.toString();
	}
	
	
	
	public static int getInvokeType(String name, String variableOwner, Class<?> ownerClass, String internalName)
	{
		if(name.equals("<init>") || name.equals("<clinit>"))
			return Opcodes.INVOKESPECIAL;
		else if(variableOwner==null)
			return Opcodes.INVOKESTATIC;
		else
			try {
				if(internalName!=null && Class.forName(Type.getObjectType(internalName).getClassName()).isInterface())
					return Opcodes.INVOKEINTERFACE;
				else
					return Opcodes.INVOKEVIRTUAL;
			} catch (ClassNotFoundException e) {
				//Maybe it's a generated class, i'll use the typical INVOKEVIRTUAL
				return Opcodes.INVOKEVIRTUAL;
			}
	}



	public static String getFieldOwner(String name, AbstractCodeObject parent)
	{
		if(parent instanceof IConfigurableMethodObject)
			return getFieldOwner(name, ((IConfigurableMethodObject)parent).getParent());
		else if(parent instanceof IConfigurableConstructorObject)
			return getFieldOwner(name, ((IConfigurableConstructorObject)parent).getParent());
		else if(parent instanceof ClassObject)
		{
			ClassObject clsObject=(ClassObject)parent;
			for(FieldObject<?> field : clsObject.getFields())
			{
				if(field.getName().equals(name))
					return translateClassName(clsObject.getName());
			}
			//not found on the main class, looking for its parents
			Class<?> tmpSuperClass=clsObject.getSuperClass();
			do {
				try {
					Field fld=clsObject.getSuperClass().getDeclaredField(name);
					return translateClassName(fld.getDeclaringClass().getName());
				} catch (Exception e) {
					//not found, we will search parent until parent is null
					tmpSuperClass=tmpSuperClass.getSuperclass();
				}
			}while(tmpSuperClass!=null);
			
		}
		
		return null;
	}



	public static ClassObject getClassObject(AbstractCodeObject parent)
	{
		if(parent instanceof IConfigurableMethodObject)
			return getClassObject(((IConfigurableMethodObject)parent).getParent());
		else if(parent instanceof IConfigurableConstructorObject)
			return getClassObject(((IConfigurableConstructorObject)parent).getParent());
		else if(parent instanceof ClassObject)
			return (ClassObject) parent;
		else
			throw new RuntimeException("Class "+parent.getClass().getName()+" not supported!!!!");
	}
	
//	public static ConstructorObject getMethodObject(AbstractCodeObject parent) {
//		if(parent instanceof MethodObjectASM31 || parent instanceof ConstructorObjectASM31)
//			return (ConstructorObject)parent;
//		else 
//			throw new RuntimeException("LinePart "+parent.getClass().getName()+" not supported!!!!");
//	}
	
	
	
	
	
	public static LoopCodePart getLoopObject(AbstractCodeObject parent) {
		if(parent instanceof LoopCodePart)
			return (LoopCodePart)parent;
		else
			return null;
	}
	
	public static SwitchCodePart getSwitchObject(AbstractCodeObject parent) {
		if(parent instanceof SwitchCodePart)
			return (SwitchCodePart)parent;
		else
			return null;
	}



	public static void checkHasReturn(ConstructorObject obj)
	{
		ICodePart codePart=obj.getCode();
		
		if(codePart instanceof CodeBlock)
		{
			CodeBlock block=(CodeBlock)codePart;
			boolean foundReturn=false;
			for(ICodePart code: block)
			{
				if(code.isReturnCodePart())
				{
					foundReturn=true;
					break;
				}
			}
			if(!foundReturn)
				block.add(ClassGeneratorFactory.getClassGenerator().lineFactory.methods.returnEmpty());
		}
		else
		{
			if(!codePart.isReturnCodePart())
			{
				CodeBlock codeBlock=new CodeBlockASM31();
				codeBlock.add(codePart);
				codeBlock.add(ClassGeneratorFactory.getClassGenerator().lineFactory.methods.returnEmpty());
				obj.setCode(codeBlock);
			}
		}
	}



	public static boolean isStatic(int[] modifiers)
	{
		return checkAccessModifier(modifiers, Opcodes.ACC_STATIC);
	}
	
	public static boolean isFinal(int[] modifiers)
	{
		return checkAccessModifier(modifiers, Opcodes.ACC_FINAL);
	}
	
	public static boolean checkAccessModifier(int modifiers[], int target)
	{
		boolean result=false;
		for(int mod : modifiers)
		{
			if(mod==target)
			{
				result=true;
				break;
			}
		}
		return result;
	}



	public static int[] translateModifiers(int modifiers)
	{
		ArrayList<Integer> arrModifiers=new ArrayList<Integer>();
		if(Modifier.isAbstract(modifiers)) arrModifiers.add(Opcodes.ACC_ABSTRACT);
		if(Modifier.isFinal(modifiers)) arrModifiers.add(Opcodes.ACC_FINAL);
		if(Modifier.isInterface(modifiers)) arrModifiers.add(Opcodes.ACC_INTERFACE);
		if(Modifier.isNative(modifiers)) arrModifiers.add(Opcodes.ACC_NATIVE);
		if(Modifier.isPrivate(modifiers)) arrModifiers.add(Opcodes.ACC_PRIVATE);
		if(Modifier.isProtected(modifiers)) arrModifiers.add(Opcodes.ACC_PROTECTED);
		if(Modifier.isPublic(modifiers)) arrModifiers.add(Opcodes.ACC_PUBLIC);
		if(Modifier.isStatic(modifiers)) arrModifiers.add(Opcodes.ACC_STATIC);
		if(Modifier.isStrict(modifiers)) arrModifiers.add(Opcodes.ACC_STRICT);
		if(Modifier.isSynchronized(modifiers)) arrModifiers.add(Opcodes.ACC_SYNCHRONIZED);
		if(Modifier.isTransient(modifiers)) arrModifiers.add(Opcodes.ACC_TRANSIENT);
		if(Modifier.isVolatile(modifiers)) arrModifiers.add(Opcodes.ACC_VOLATILE);
		
		int result[]=new int[arrModifiers.size()];
		for(int i=0;i<result.length;i++)
			result[i]=arrModifiers.get(i);
		
		return result;
	}



	public static int getCastNeeded(Type originType, Type targetType)
	{
		Exception internalException=null;
		
		//If types are not objects and are equal, no more checking needed
		if(targetType.getSort()!=Type.OBJECT && targetType.getSort()==originType.getSort())
			return Opcodes.NOP;
		
		//checking for conversion as they are object or not equal
		switch(targetType.getSort())
		{
			//if the target is an object, i'll if it's directly assignable. If not, i'll throw an exception
			case Type.OBJECT:
				try
				{
					Class<?> targetClass=Class.forName(targetType.getClassName());
					Class<?> originClass=Class.forName(originType.getClassName());
					
					if(targetClass.isAssignableFrom(originClass)) //abstract classes
						return Opcodes.NOP;
					else if(originClass.isInterface())
					{
						for(Class<?> iface : targetClass.getInterfaces())
						{
							if(iface.isAssignableFrom(originClass)) //abstract classes
								return Opcodes.NOP;
						}
					}
					break;
					
				}catch(Exception ex)
				{
					internalException=ex;
					break;
				}
			
			//if the target is an array, the source must be also an array of the same type (not conversion needed between array types)
			case Type.ARRAY:
				if(originType.getSort()==Type.ARRAY)
					if(getCastNeeded(originType.getElementType(), targetType.getElementType())==Opcodes.NOP)
						return Opcodes.NOP;
				break;
			
			
				
			//byte only possible from integer
			case Type.BYTE:
				if(originType.getClassName().equals(Byte.class.getName())) //autoboxing
					return Opcodes.NOP;
				if(originType.getSort()==Type.INT || originType.getClassName().equals(Integer.class.getName()))
					return Opcodes.I2B;
				break;
			
			//char only possible from integer
			case Type.CHAR:
				if(originType.getClassName().equals(Character.class.getName())) //autoboxing
					return Opcodes.NOP;
				if(originType.getSort()==Type.INT || originType.getClassName().equals(Integer.class.getName()))
					return Opcodes.I2C;
				break;
				
			//short only possible from integer
			case Type.SHORT:
				if(originType.getClassName().equals(Short.class.getName())) //autoboxing
					return Opcodes.NOP;
				if(originType.getSort()==Type.INT || originType.getClassName().equals(Integer.class.getName()))
					return Opcodes.I2S;
				break;
			
			//Double possible from int, long and float
			case Type.DOUBLE:
				if(originType.getClassName().equals(Double.class.getName())) //autoboxing
					return Opcodes.NOP;
				if(originType.getSort()==Type.INT || originType.getClassName().equals(Integer.class.getName()))
					return Opcodes.I2D;
				if(originType.getSort()==Type.LONG || originType.getClassName().equals(Long.class.getName()))
					return Opcodes.L2D;
				if(originType.getSort()==Type.FLOAT || originType.getClassName().equals(Float.class.getName()))
					return Opcodes.F2D;
				break;
			
			//Float possible from int, long and double 
			case Type.FLOAT:
				if(originType.getClassName().equals(Float.class.getName())) //autoboxing
					return Opcodes.NOP;
				if(originType.getSort()==Type.INT || originType.getClassName().equals(Integer.class.getName()))
					return Opcodes.I2F;
				if(originType.getSort()==Type.LONG || originType.getClassName().equals(Long.class.getName()))
					return Opcodes.L2F;
				if(originType.getSort()==Type.DOUBLE || originType.getClassName().equals(Double.class.getName()))
					return Opcodes.D2F;
				break;
				
			//Int possible from Long, Float and Double
			case Type.INT:
				if(originType.getClassName().equals(Integer.class.getName())) //autoboxing
					return Opcodes.NOP;
				if(originType.getSort()==Type.LONG || originType.getClassName().equals(Long.class.getName()))
					return Opcodes.L2I;
				if(originType.getSort()==Type.FLOAT || originType.getClassName().equals(Float.class.getName()))
					return Opcodes.F2I;
				if(originType.getSort()==Type.DOUBLE || originType.getClassName().equals(Double.class.getName()))
					return Opcodes.D2I;
				break;
			
			//Long possible from Int, Float and Double
			case Type.LONG:
				if(originType.getClassName().equals(Long.class.getName())) //autoboxing
					return Opcodes.NOP;
				if(originType.getSort()==Type.INT || originType.getClassName().equals(Integer.class.getName()))
					return Opcodes.I2L;
				if(originType.getSort()==Type.FLOAT || originType.getClassName().equals(Float.class.getName()))
					return Opcodes.F2L;
				if(originType.getSort()==Type.DOUBLE || originType.getClassName().equals(Double.class.getName()))
					return Opcodes.D2L;
				break;
				
			//void and boolean doesn't admit conversion
			case Type.BOOLEAN:
				if(originType.getClassName().equals(Boolean.class.getName())) //autoboxing
					return Opcodes.NOP;
				break;
			case Type.VOID:
				break;
				
			default:
				break;
		
		}
		
		//if code reach here, there is no direct conversion
		//i'll return a CAST instruction if it's an object
		if(originType.getSort()==Type.OBJECT)
			return Opcodes.CHECKCAST;

		//if it's a primitive, there is no possible conversion 
		RuntimeException rex=new RuntimeException("Target variable type ("+targetType.getClassName()+") incompatible with origin type ("+originType.getClassName()+")");
		if(internalException!=null)
			rex.initCause(internalException);
		throw rex;
	}



	public static String[] getClassesNames(List<Class<? extends Throwable>> exceptions)
	{
		if(exceptions==null || exceptions.size()==0)
			return null;
		
		String result[]=new String[exceptions.size()];
		for(int i=0;i<result.length;i++)
			result[i]=Type.getInternalName(exceptions.get(i));
			
		return result;
	}



	







//	private static Class<?>[] parametersToClasses(ParameterObject<?>[] params)
//	{
//		if(params==null || params.length==0)
//			return null;
//		
//		Class<?> result[]=new Class<?>[params.length];
//		for(int i=0;i<result.length;i++)
//			result[i]=params[i].getClassType();
//		
//		return result;
//	}
	
	
	

}
